package org.express.portal;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.express.portal.container.ContainerFactory;
import org.express.portal.converter.ConverterFactory;
import org.express.portal.persist.DBManager;
import org.express.portal.renderer.JavaScriptRenderer;
import org.express.portal.renderer.Renderer;
import org.express.portal.renderer.TextRenderer;
import org.express.portal.template.JspTemplateFactory;
import org.express.portal.template.TemplateFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Dispatcher handles ALL requests from clients, and dispatches to appropriate
 * handler to handle each request.
 * 
 * @author Nixin
 */
class Dispatcher
{
	private final Logger log = LoggerFactory.getLogger(getClass());

	private ServletContext servletContext;
	private ContainerFactory containerFactory;
	private boolean multipartSupport = false;
	private long maxFileSize = 10L * 1024L * 1024L; // default to 10M.
	private UrlMatcher[] urlMatchers = null;
	private Map<UrlMatcher, Action> urlMap = new HashMap<UrlMatcher, Action>();
	private ConverterFactory converterFactory = new ConverterFactory();
	private Interceptor[] interceptors = null;
	private ExceptionHandler exceptionHandler = null;

	public void init(Config config) throws ServletException
	{
		log.info("Init Dispatcher...");
		this.servletContext = config.getServletContext();
		try
		{
			initAll(config);
		}
		catch (ServletException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new ServletException("Dispatcher init failed.", e);
		}
	}

	void initAll(Config config) throws Exception
	{
		// detect multipart support:
		try
		{
			Class.forName("org.apache.commons.fileupload.servlet.ServletFileUpload");
			this.multipartSupport = true;
			log.info("Using CommonsFileUpload to handle multipart http request.");
			String maxFileSize = config.getInitParameter("maxFileSize");
			if (maxFileSize != null)
			{
				try
				{
					long n = Evaluator.eval(maxFileSize);
					if (n <= 0)
						throw new NumberFormatException();
					this.maxFileSize = n;
				}
				catch (NumberFormatException e)
				{
					log.warn("Invalid parameter <maxFileSize> value '" + maxFileSize + "', using default.");
				}
			}
		}
		catch (ClassNotFoundException e)
		{
			log.info("CommonsFileUpload not found. Multipart http request can not be handled.");
		}

		// init IoC container:
		String containerName = config.getInitParameter("container");
		if (containerName == null)
			throw new ConfigException("Missing init parameter <container>.");
		this.containerFactory = Utils.createContainerFactory(containerName);
		this.containerFactory.init(config);
		List<Object> beans = this.containerFactory.findAllBeans();
		initComponents(beans);

		// init template engine:
		initTemplateFactory(config);
	}

	void initTemplateFactory(Config config)
	{
		String name = config.getInitParameter("template");
		if (name == null)
		{
			name = JspTemplateFactory.class.getName();
			log.info("No template factory specified. Default to '" + name + "'.");
		}
		TemplateFactory tf = Utils.createTemplateFactory(name);
		tf.init(config);
		log.info("Template factory '" + tf.getClass().getName() + "' init ok.");
		TemplateFactory.setTemplateFactory(tf);
	}

	void initComponents(List<Object> beans)
	{
		List<Interceptor> intList = new ArrayList<Interceptor>();
		for (Object bean : beans)
		{
			if (bean instanceof Interceptor)
				intList.add((Interceptor) bean);
			if (this.exceptionHandler == null && bean instanceof ExceptionHandler)
				this.exceptionHandler = (ExceptionHandler) bean;
			addActions(bean);
		}
		if (this.exceptionHandler == null)
			this.exceptionHandler = new DefaultExceptionHandler();
		this.interceptors = intList.toArray(new Interceptor[intList.size()]);
		// sort interceptors by its annotation of 'InterceptorOrder':
		Arrays.sort(this.interceptors, new Comparator<Interceptor>()
		{
			public int compare(Interceptor i1, Interceptor i2)
			{
				InterceptorOrder o1 = i1.getClass().getAnnotation(InterceptorOrder.class);
				InterceptorOrder o2 = i2.getClass().getAnnotation(InterceptorOrder.class);
				int n1 = o1 == null ? Integer.MAX_VALUE : o1.value();
				int n2 = o2 == null ? Integer.MAX_VALUE : o2.value();
				if (n1 == n2)
					return i1.getClass().getName().compareTo(i2.getClass().getName());
				return n1 < n2 ? (-1) : 1;
			}
		});
		this.urlMatchers = urlMap.keySet().toArray(new UrlMatcher[urlMap.size()]);
		// sort url matchers by its url:
		Arrays.sort(this.urlMatchers, new Comparator<UrlMatcher>()
		{
			public int compare(UrlMatcher o1, UrlMatcher o2)
			{
				String u1 = o1.url;
				String u2 = o2.url;
				int n = u1.compareTo(u2);
				if (n == 0)
					throw new ConfigException("Cannot mapping one url '" + u1 + "' to more than one action method.");
				return n;
			}
		});
	}

	// find all action methods and add them into urlMap:
	void addActions(Object bean)
	{
		Class<?> clazz = bean.getClass();
		Method[] ms = clazz.getMethods();
		for (Method m : ms)
		{
			if (isActionMethod(m))
			{
				Mapping mapping = m.getAnnotation(Mapping.class);
				String url = mapping.value();
				UrlMatcher matcher = new UrlMatcher(url);
				if (matcher.getArgumentCount() != m.getParameterTypes().length)
				{
					warnInvalidActionMethod(m, "Arguments in URL '" + url + "' does not match the arguments of method.");
					continue;
				}
				log.info("Mapping url '" + url + "' to method '" + m.toGenericString() + "'.");
				urlMap.put(matcher, new Action(bean, m));
			}
		}
	}

	// check if the specified method is a vaild action method:
	boolean isActionMethod(Method m)
	{
		Mapping mapping = m.getAnnotation(Mapping.class);
		if (mapping == null)
			return false;
		if (mapping.value().length() == 0)
		{
			warnInvalidActionMethod(m, "Url mapping cannot be empty.");
			return false;
		}
		if (Modifier.isStatic(m.getModifiers()))
		{
			warnInvalidActionMethod(m, "method is static.");
			return false;
		}
		Class<?>[] argTypes = m.getParameterTypes();
		for (Class<?> argType : argTypes)
		{
			if (!converterFactory.canConvert(argType))
			{
				warnInvalidActionMethod(m, "unsupported parameter '" + argType.getName() + "'.");
				return false;
			}
		}
		Class<?> retType = m.getReturnType();
		if (retType.equals(void.class) || retType.equals(String.class) || Renderer.class.isAssignableFrom(retType))
			return true;
		warnInvalidActionMethod(m, "unsupported return type '" + retType.getName() + "'.");
		return false;
	}

	// log warning message of invalid action method:
	void warnInvalidActionMethod(Method m, String string)
	{
		log.warn("Invalid Action method '" + m.toGenericString() + "': " + string);
	}

	public boolean service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
	{
		String url = req.getRequestURI();
		String path = req.getContextPath();
		if (path.length() > 0)
			url = url.substring(path.length());
		// set default character encoding to "utf-8" if encoding is not set:
		if (req.getCharacterEncoding() == null)
			req.setCharacterEncoding("UTF-8");
		if (log.isDebugEnabled())
			log.debug("Handle for URL: " + url);
		Execution execution = null;
		for (UrlMatcher matcher : this.urlMatchers)
		{
			String[] args = matcher.getMatchedParameters(url);
			if (args != null)
			{
				Action action = urlMap.get(matcher);
				Object[] arguments = new Object[args.length];
				for (int i = 0; i < args.length; i++)
				{
					Class<?> type = action.arguments[i];
					if (type.equals(String.class))
						arguments[i] = args[i];
					else
						arguments[i] = converterFactory.convert(type, args[i]);
				}
				execution = new Execution(req, resp, action, arguments);
				break;
			}
		}
		if (execution != null)
		{
			handleExecution(execution, req, resp);
		}
		return execution != null;
	}

	void handleExecution(Execution execution, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
	{
		if (this.multipartSupport)
		{
			if (MultipartHttpServletRequest.isMultipartRequest(request))
			{
				request = new MultipartHttpServletRequest(request, maxFileSize);
			}
		}
		ActionContext.setActionContext(servletContext, request, response);
		try
		{
			InterceptorChainImpl chains = new InterceptorChainImpl(interceptors);
			chains.doInterceptor(execution);
			handleResult(request, response, chains.getResult());
		}
		catch (Exception e)
		{
			handleException(request, response, e);
		}
		finally
		{
			ActionContext.removeActionContext();
			DBManager.closeConnection();
		}
	}

	void handleException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws ServletException, IOException
	{
		try
		{
			exceptionHandler.handle(request, response, ex);
		}
		catch (ServletException e)
		{
			throw e;
		}
		catch (IOException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new ServletException(e);
		}
	}

	void handleResult(HttpServletRequest request, HttpServletResponse response, Object result) throws Exception
	{
		if (result == null)
			return;
		if (result instanceof Renderer)
		{
			Renderer r = (Renderer) result;
			r.render(this.servletContext, request, response);
			return;
		}
		if (result instanceof String)
		{
			String s = (String) result;
			if (s.startsWith("redirect:"))
			{
				response.sendRedirect(s.substring("redirect:".length()));
				return;
			}
			if (s.startsWith("script:"))
			{
				String script = s.substring("script:".length());
				new JavaScriptRenderer(script).render(servletContext, request, response);
				return;
			}
			new TextRenderer(s).render(servletContext, request, response);
			return;
		}
		throw new ServletException("Cannot handle result with type '" + result.getClass().getName() + "'.");
	}

	public void destroy()
	{
		log.info("Destroy Dispatcher...");
		this.containerFactory.destroy();
	}
}
